/* * DG (DG Gaming Engine)  - API
 * Copyright (C) 2005  Bob Marks (marksie531@yahoo.com)
 * http://code.google.com/p/dng-gaming
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package com.dg.gaming.client.gui;

import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import info.clearthought.layout.TableLayout;
import nanoxml.XMLElement;
import com.dg.gaming.client.*;
import com.dg.gaming.api.common.*;
import com.dg.gaming.api.common.msg.*;
import com.dg.gaming.api.common.util.*;

/**
 * DG panel which exists in can be put within an applet
 * frame or a application frame.
 *
 * @author Bob Marks
 * @author starsinthesky
 * @author Gman
 * @version Alpha 0.2.3
 */
public class DGClientPanel extends JPanel implements IClient,
        Observer {

    // Frame variables
    public static final int FRAME_HEIGHT = 435;
    public static final int FRAME_WIDTH = 590;
    public static final int SPACING = 5;

    // Connection to server
    private ClientConnectionThread conn = null;

    // Data class for receiving data specific Comm messages


    // GUI stuff
    private JTableList tableListBox;
    private JUserList userListBox;

    // buttons
    private DGButton infoButton, messageButton;
    private DGButton newGameButton, joinButton;
    private ChatGameComponent messageBox;
    private DGButton rulesButton;

    // Declare a Hash of DGTableFrames and private messages
    private HashMap tableFramesHash;
    private HashMap privateMessagesHash;

    // Resources classes
    private DGLabels DGLabels; // API resources

    private IGameClientGUI dgClientGui = null;

    /**
     * Constructor for a client frame.
     *
     * @param conn            Connection to server.
     * @param gameClientGui  Connection to frame / applet.
     */
    public DGClientPanel(ClientConnectionThread conn,
                            IGameClientGUI dgClientGui) {
        // Set up fields
        this.conn = conn;
        this.conn.getTableList().addObserver(this);
        this.conn.getUserList().addObserver(this);

        this.dgClientGui = dgClientGui;

        this.DGLabels = DGLabels.getInstance();
        this.tableFramesHash = new HashMap();
        this.privateMessagesHash = new HashMap();
    }

    /**
     * Sets up the graphical user interface.
     */
    public void setUpGUI() {

        // Create Tablelayout variables
        double pref = TableLayout.PREFERRED, fill = TableLayout.FILL;
        double[][] sizes;

        // Create user and table panels
        sizes = new double[][] { {fill}, {pref, SPACING, fill, SPACING, pref}
        };
        DGPanel userPanel = new DGPanel(sizes);

        // Set background colour
        setBackground(GameProperties.getBackgroundColour());

        // Create table panel
        DGPanel tablePanel = new DGPanel(sizes);
        this.userListBox = new JUserList(conn.getGame());
        DGScrollPane userListScroll = new DGScrollPane(userListBox);

        // Create user buttons panel and add buttons to it
        sizes = new double[][] { {pref, SPACING, pref}, {pref}
        };
        DGPanel userButtonPanel = new DGPanel(sizes);
        this.infoButton = new DGButton(this.DGLabels.get("info"));
        this.messageButton = new DGButton(this.DGLabels.get("message"));
        userButtonPanel.add(this.infoButton, "0, 0");
        userButtonPanel.add(this.messageButton, "2, 0");

        // Add user panel stuff.
        userPanel.add(userButtonPanel, "0, 0");
        userPanel.add(userListScroll, "0, 2");
        userPanel.add(new RatingsPanel(), "0, 4");
        userPanel.setBorder(BorderFactory.createTitledBorder
                            (BorderFactory.createEtchedBorder(),
                             this.DGLabels.get("user.list")));

        // Create table panel
        sizes = new double[][] { {pref, SPACING, pref, SPACING, pref}, {pref}
        };
        DGPanel tableButtonPanel = new DGPanel(sizes);

        // radio buttons
        this.newGameButton = new DGButton(this.DGLabels.get("new.game"));
        this.joinButton = new DGButton(this.DGLabels.get("join"));
        this.rulesButton = new DGButton(this.DGLabels.get("rules"));

        // Add items to main panel
        tableButtonPanel.add(this.newGameButton, "0,0");
        tableButtonPanel.add(this.joinButton, "2,0");
        tableButtonPanel.add(this.rulesButton, "4,0");

        // Create table list and add components to panel
        this.tableListBox = new JTableList(conn.getGame(), this.dgClientGui);
        DGScrollPane tableListScroll = new DGScrollPane(this.tableListBox);
        tablePanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.
                createEtchedBorder(),
                             this.DGLabels.get("current.games.being.played")));
        tablePanel.add(tableButtonPanel, "0,0");
        tablePanel.add(tableListScroll, "0,2");

        // Create bottom right panel
        this.messageBox = new ChatGameComponent(conn, 0);
        this.messageBox.receiveMessage(DGLabels.getInstance().get("labels"),
                                       GameLabels.getWelcomeMessage());

        // Create main panel
        sizes = new double[][] { {0.3, 0.7}, {50, TableLayout.FILL}
        };
        setLayout(new TableLayout(sizes));
        setBackground(GameProperties.getBackgroundColour());

        // Create right panel
        sizes = new double[][] { {TableLayout.FILL}, {0.5, 0.5}
        };
        DGPanel rightPanel = new DGPanel(sizes);

        // Add panels
        rightPanel.add(tablePanel, "0,0");
        rightPanel.add(messageBox, "0,1");

        // Add to this panel
        add(new DGTitlePanel(), "0,0,1,0");
        add(userPanel, "0,1");
        add(rightPanel, "1,1");

        // Set the title
        String title = GameLabels.getClientTitle() + " - " +
                       DGLabels.get("connected.as");
        dgClientGui.setUITitle(title + " [" + this.conn.getUserId() + "]");

        // Update button states
        updateButtonStates();
        invalidate();
        validate();
    }

    /**
     * Update the button states depending on how many users are logged on and
     * what components are selected.
     */
    private void updateButtonStates() {
        // assume default state (no users or tables exist)
        boolean messageButtonEnabled = false;
        boolean infoButtonEnabled = false;
        boolean newGameButtonEnabled = false;
        boolean joinButtonEnabled = false;

        if (this.conn != null) {
            // Retrieve username
            long username = conn.getUserId();
            TableList tables = conn.getTableList();

            // Read this from server eventually
            newGameButtonEnabled =
                    this.conn.getTableList().size() <
                    GameProperties.getMaxNumOfTables() &&
                    tables.getNumOfTablesUserOwns(username) <
                    GameProperties.getMaxNumOfTablesPerUser();

            // Rule 1: At least two users exist (yourself and someone else)
            //         and a row in the user table is selected
            boolean rule1 = this.userListBox.getSelectedIndex() != -1;

            if (rule1) {
                User selectedUser = (User) userListBox.getSelectedValue();
                messageButtonEnabled = selectedUser.getUserid()!=
                        username;
            }

            // Rule 2: At least one table exists and a row in the table is selected
            boolean rule2 = this.conn.getTableList().size() > 0 &&
                            this.tableListBox.getSelectedIndex() != -1;

            // Rule 3: Rule 2 AND user isn't joining a game where he is already sitting
            //         AND the game is a public game.
            boolean rule3 = rule2;
            if (rule3) { // ensure rule 2 other no table exists
                Table table = this.tableListBox.getSelectedTable();
                rule3 = (!table.containsPlayer(conn.getUserId()) &&
                         table.isPublic());
            }

            // Update enabled booleans depending on rules
            if (rule1) {
                infoButtonEnabled = true;
            }
            if (rule3) {
                joinButtonEnabled = true;
            }
        }
        // Update button states
        this.infoButton.setEnabled(infoButtonEnabled);
        this.messageButton.setEnabled(messageButtonEnabled);
        this.newGameButton.setEnabled(newGameButtonEnabled);
        this.joinButton.setEnabled(joinButtonEnabled);

        // The rules button is enabled if there is a rules.file property and is
        // disabled if there isn't.
        boolean rulesButtonEnabled =
                (GameLabels.getRulesFilename() != null) ||
                (GameProperties.getRulesFilename() != null);
        this.rulesButton.setEnabled(rulesButtonEnabled);
    }

    /**
     *  Add listeners to the various buttons and lists
     */
    public void addListeners() {
        // Listener for the info button.
        this.infoButton.addActionListener(
                new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                dgClientGui.getUserDialog(userListBox.getSelectedUser());
            }
        }
        );

        // Declare listener for the message button.
        this.messageButton.addActionListener(
                new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Create a new private message component
                getPrivateMessageComponent(userListBox.getSelectedUsername());
            }
        }
        );

        // Declare listener for the new game button.
        this.newGameButton.addActionListener(
                new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                dgClientGui.getPropertyDialog(conn);
            }
        }
        );

        // Listener for the join button
        this.joinButton.addActionListener(
                new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                doJoinButtonAction();
            }
        }
        );

        // Listener for the rules button
        this.rulesButton.addActionListener(
                new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                dgClientGui.getRulesDialog();
            }
        }
        );

        // Listener on the table list
        this.tableListBox.addListSelectionListener(
                new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent listselectionevent) {
                updateButtonStates();
            }
        }
        );

        // Double-click listener on the table list
        this.tableListBox.addMouseListener(
                new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                // If this is a double click and the joinButton is enabled...
                if ((e.getClickCount() == 2) &&
                    joinButton.isEnabled()) {
                    // ... then do the same action as clicking the join button
                    doJoinButtonAction();
                }
            }
        }
        );

        // Listener on the user list
        this.userListBox.addListSelectionListener(
                new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent listselectionevent) {
                updateButtonStates();
            }
        }
        );
    }

    /**
     * This is the action performed when the "Join Table" button is pressed.
     *
     * This is a separate function because the join table action can be caused
     * by either clicking the "Join Table" button, or by double clicking a
     * table in the table list.
     */
    private void doJoinButtonAction() {
        // Retrieve the current selected table number
        String selectedTable = tableListBox.getSelectedTableNum();

        // Create join table request and send to server.
        MsgJoinTable joinTable = new MsgJoinTable(selectedTable);
        conn.send(joinTable);
    }

    /**
     * @see com.dg.gaming.client.gui.IClient#receiveGameMessage(nanoxml.XMLElement)
     */
    public void receiveGameMessage(XMLElement message) {
        try {
            String messageType = message.getName();

            if (messageType.equals(Msg.INVITE)) {
                MsgInvite commInvite = new MsgInvite(message);
                receiveInvite(commInvite);
            } else if (messageType.equals(Msg.CHAT_GAME)) {
                MsgChatClient commMessage = new MsgChatClient(message);
                receiveGameChat(commMessage);
            } else if (messageType.equals(Msg.CHAT_PRIVATE)) {
                MsgChatPrivate commMessage = new MsgChatPrivate(message);
                receivePrivateChat(commMessage);
            } else if (messageType.equals(Msg.ERROR)) {
                MsgError commError = new MsgError(message);
                receiveError(commError);
            }

            // Also pass message into adapter method which all DG clients
            // can recieve to do additional processing if this is required.
            receiveMessage(message);
        } catch (TransmissionException transEx) {}
    }

    /**
     * Adapter method for receiving a custom message.
     *
     * @see com.dg.gaming.client.gui.ITable#receiveTableMessage(nanoxml.XMLElement)
     */
    protected void receiveMessage(XMLElement message) {}

    /**
     * Recieve a table message and delegate it to the correct frame.
     *
     * @see com.dg.gaming.client.gui.ITable#receiveTableMessage(nanoxml.XMLElement)
     */
    public void receiveTableMessage(XMLElement message, String tableNum) {
        // Redirect to correct frame depending on table number
        DGTableFrame frame = getTableFrame(tableNum);
        if (frame != null) {
            frame.receiveTableMessage(message, tableNum);
        }
    }

    /**
     * Close the server back down again.
     */
    public void closeClient() {
        try {
            // Try to close connection to server
            MsgDisconnect commDisconnect = new MsgDisconnect();
            conn.send(commDisconnect);

            // Close down various Tables
            Vector keys = new Vector(tableFramesHash.keySet());

            // Loop through each table and remove the player
            for (int i = 0; i < keys.size(); i++) {
                String tableNum =  keys.get(i).toString();
                getTableFrame(tableNum).dispose();
            }
        } catch (Exception ex) {}
        if (!DGUtils.isApplet()) {
            System.exit(0);
        }
    }

    /**
     * Method which receives a message from the server (or user)
     *
     * @param chatMessage  Chat message from user
     */
    public void receiveGameChat(MsgChatClient chatMessage) {
        long usernameFrom = chatMessage.getUserid();
        String messageText = chatMessage.getChat();

        messageBox.receiveMessage(usernameFrom+"", messageText);
    }

    /**
     * Receive private chat message.
     *
     * @param chatMessage
     */
    public void receivePrivateChat(MsgChatPrivate chatMessage) {
        long usernameFrom = chatMessage.getUserid();
        String messageText = chatMessage.getChat();

        getPrivateMessageComponent(usernameFrom).recieveMessage(usernameFrom+"",
                messageText);
    }

    /**
     * Receive an error message.
     *
     * @param errorMessage   The error message from the server
     */
    private void receiveError(MsgError errorMessage) {
        String errorStr = DGLabels.getError(errorMessage.getStatus());
        JOptionPane.showMessageDialog(
                this, errorStr, DGLabels.get("error"),
                JOptionPane.INFORMATION_MESSAGE
                );
    }

    /**
     * Return the correct DGTableFrame.  This method also adds a listener
     * so that when it closes down it does so cleanly.
     *
     * @param table  Specified table.
     * @return       Return a DGTableFrame which has been stored in cauche
     */
    protected DGTableFrame getTableFrame(final Table table) {
        DGTableFrame newTableFrame = null;
        Integer iTableNum = new Integer(table.getTableNum());

        // Check hash to see if the frame already exists
        if (tableFramesHash.containsKey(iTableNum)) {
            // retrieve existing message component
            newTableFrame = (DGTableFrame) tableFramesHash.get(iTableNum);
        } else { // otherwise create a new frame
            // retrieve the table
            TableConnectionThread tableConn = new TableConnectionThread(conn,
                    table.getTableNum());
            newTableFrame = dgClientGui.getDGTableFrame(tableConn);
            newTableFrame.setTableFramesHash(tableFramesHash);

            tableFramesHash.put(iTableNum, newTableFrame);
        }

        // return table frame
        return newTableFrame;
    }

    /**
     * Overloaded version when you are sure that the table exists (other wise
     * returns null).
     *
     * @param tableNum  Specified table number.
     * @return          Correct DGTableFrame e.g. ChessTableFrame
     */
    protected DGTableFrame getTableFrame(String tableNum) {
        Table table = conn.getTableList().getTable(tableNum);

        // If player no longer is within table, don't open table
        if (table != null && !table.containsPlayer(this.conn.getUserId())) {
            return null;
        }

        // If table exists, retrieve frame and send the message to it
        if (table != null) {
            return getTableFrame(table);
        }
        return null;
    }

    /**
     * Close the table frame.
     *
     * @param table
     */
    private void closeTableFrame(Table table) {
        String tableNum = table.getTableNum();

        // Remove frame from hash
        tableFramesHash.remove(new Integer(tableNum));

        // Remove table from data
        conn.getTableList().removePlayer(tableNum, conn.getUserId());
    }

    /**
     * Recieve invite communications object.
     *
     * @param invite
     */
    public void receiveInvite(MsgInvite invite) {
        // Retreive table number from message
        String tableNum = invite.getTableNum();
        Table table = conn.getTableList().getTable(tableNum);

        // Check to see if this is an invite or an invite reply
        if (invite.getStatus() == MsgInvite.REQUEST) {
            // Compose string for user

            String tableVisibility = table.isPublic() ?
                                     DGLabels.get("public") :
                                     DGLabels.get("private");

            String messageTitle;
            try {
                // Try the old style message composition
                messageTitle =
                        DGLabels.get("player") + " " + invite.getUserid() +
                        " " +
                        DGLabels.get("requests.a.game.playing.at") + " " +
                        DGLabels.get("table") + " " + invite.getTableNum() +
                        " (" + tableVisibility + "). " +
                        DGLabels.get("accept") + "?";
            } catch (MissingResourceException mrEx) {
                // That didn't work, so try try new style message composition
                Object[] textArgs = {
                                    invite.getUserid(),
                                    new Integer(invite.getTableNum()),
                                    tableVisibility
                };

                messageTitle = DGLabels.get("table.invite", textArgs);
            }

            int answer = JOptionPane.showConfirmDialog(
                    this,
                    messageTitle,
                    DGLabels.get("game.request"),
                    JOptionPane.YES_NO_OPTION
                         );

            // Convert to a number the CommInvite object understands
            int yesNo = (answer == JOptionPane.YES_OPTION) ?
                        MsgInvite.ACCEPT : MsgInvite.DECLINE;

            // Create invite comm object to reply to the users
            MsgInvite inviteReply = new MsgInvite(
                    yesNo, invite.getTableNum());
            inviteReply.setUsername(invite.getUserid());

            // Transmit message back to the server
            conn.send(inviteReply);
        }
        // User has declined invite
        else if (invite.getStatus() == MsgInvite.DECLINE) {

            // report message that user has declined the offer
            String messageTitle;
            try {
                // Try the old style message composition
                messageTitle =
                        DGLabels.get("user") + ": " +
                        invite.getUsernameTo() + " " +
                        DGLabels.get("has.rejected.offer.at.table") + " " +
                        invite.getTableNum();
            } catch (MissingResourceException mrEx) {
                // That didn't work, so try try new style message composition
                Object[] textArgs = {
                                    invite.getUsernameTo(),
                                    new Integer(invite.getTableNum())
                };

                messageTitle = DGLabels.get("rejected.invite", textArgs);
            }

            JOptionPane.showMessageDialog(
                    this, messageTitle, DGLabels.get("game.request"),
                    JOptionPane.INFORMATION_MESSAGE
                    );
        }
    }

    /**
     * Returns a private message for a particular user and creates one if it
     * doesn't already exist.
     *
     * @param usernameTo  Username of person being chatted to.
     * @return            Return a chat message component.
     */
    protected ChatPrivateDialog getPrivateMessageComponent(final long
            usernameTo) {
        ChatPrivateDialog privMessage = null;

        if (privateMessagesHash.containsKey(usernameTo)) {
            // retrieve existing message component
            privMessage = (ChatPrivateDialog) privateMessagesHash.get(
                    usernameTo);
        } else {
            // Create new private message component and add to the hash
            privMessage = dgClientGui.getChatPrivateDialog(usernameTo, conn);

            // Add a listener for this message
            privMessage.addWindowListener(
                    new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    privateMessagesHash.remove(usernameTo);
                }
            }
            );

            privateMessagesHash.put(usernameTo, privMessage);
        }

        // return message component
        return privMessage;
    }

    /**
     * Update method on observer when the data changes.  This updates
     * the state of the buttons and opens / closes any game frames.
     *
     * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
     */
    public void update(Observable obs, Object obj) {
        updateButtonStates();

        // Loop through the tables and see if the user has created a new game
        // or joined an existing one
        long username = conn.getUserId();
        TableList tableList = conn.getTableList();
        String[] tableNumbers = tableList.getTablesNumbers();
        for (int i = 0; i < tableNumbers.length; i++) {
            String tableNum = tableNumbers[i];

            Table table = tableList.getTable(tableNum);

            // Do a check to see if this user is at a table and the frame for
            // this particular table is created (in the hash)
            if (table.containsPlayer(username) &&
                !tableFramesHash.containsKey(new Integer(table.getTableNum()))) {

                // Frame doesn't exist so create it
                getTableFrame(table);
            }
        }
    }
}
